﻿// Copyright (c) Athena Dev Teams - Licensed under GNU GPL

/// DES (Data Encryption Standard) algorithm, modified version.
/// @see http://www.eathena.ws/board/index.php?autocom=bugtracker&showbug=5099.
/// @see http://en.wikipedia.org/wiki/Data_Encryption_Standard
/// @see http://en.wikipedia.org/wiki/DES_supplementary_material
/// 
/// All credits on DES algorithm goes to eA team, my only work here was traslate it to C#
/// 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ROToolBelt.IO.GRF
{
    public class GrfDecoder
    {
        private const int BLOCK_SIZE = sizeof(long);

        private readonly byte[] mask;
        private readonly byte[] ip_table;
        private readonly byte[][] s_table;
        private readonly byte[] fp_table;
        private readonly byte[] tp_table;

        public GrfDecoder()
        {
            /// Bitmask for accessing individual bits of a byte.
            this.mask = new byte[8] { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 };

            this.ip_table = new byte[64] {
                                            58, 50, 42, 34, 26, 18, 10,  2,
                                            60, 52, 44, 36, 28, 20, 12,  4,
                                            62, 54, 46, 38, 30, 22, 14,  6,
                                            64, 56, 48, 40, 32, 24, 16,  8,
                                            57, 49, 41, 33, 25, 17,  9,  1,
                                            59, 51, 43, 35, 27, 19, 11,  3,
                                            61, 53, 45, 37, 29, 21, 13,  5,
                                            63, 55, 47, 39, 31, 23, 15,  7, 
                                         };

            this.tp_table = new byte[32] {
                                            16,  7, 20, 21,
                                            29, 12, 28, 17,
                                             1, 15, 23, 26,
                                             5, 18, 31, 10,
                                             2,  8, 24, 14,
                                            32, 27,  3,  9,
                                            19, 13, 30,  6,
                                            22, 11,  4, 25, 
                                         };

            this.fp_table = new byte[64] {
                                            40,  8, 48, 16, 56, 24, 64, 32,
                                            39,  7, 47, 15, 55, 23, 63, 31,
                                            38,  6, 46, 14, 54, 22, 62, 30,
                                            37,  5, 45, 13, 53, 21, 61, 29,
                                            36,  4, 44, 12, 52, 20, 60, 28,
                                            35,  3, 43, 11, 51, 19, 59, 27,
                                            34,  2, 42, 10, 50, 18, 58, 26,
                                            33,  1, 41,  9, 49, 17, 57, 25,
                                        };

            this.s_table = new byte[4][];
            this.s_table[0] = new byte[64] {        
                                                0xef, 0x03, 0x41, 0xfd, 0xd8, 0x74, 0x1e, 0x47,  0x26, 0xef, 0xfb, 0x22, 0xb3, 0xd8, 0x84, 0x1e,
                                                0x39, 0xac, 0xa7, 0x60, 0x62, 0xc1, 0xcd, 0xba,  0x5c, 0x96, 0x90, 0x59, 0x05, 0x3b, 0x7a, 0x85,
                                                0x40, 0xfd, 0x1e, 0xc8, 0xe7, 0x8a, 0x8b, 0x21,  0xda, 0x43, 0x64, 0x9f, 0x2d, 0x14, 0xb1, 0x72,
                                                0xf5, 0x5b, 0xc8, 0xb6, 0x9c, 0x37, 0x76, 0xec,  0x39, 0xa0, 0xa3, 0x05, 0x52, 0x6e, 0x0f, 0xd9,
                                           };

            this.s_table[1] = new byte[64] {
                                                0xa7, 0xdd, 0x0d, 0x78, 0x9e, 0x0b, 0xe3, 0x95,  0x60, 0x36, 0x36, 0x4f, 0xf9, 0x60, 0x5a, 0xa3,
                                                0x11, 0x24, 0xd2, 0x87, 0xc8, 0x52, 0x75, 0xec,  0xbb, 0xc1, 0x4c, 0xba, 0x24, 0xfe, 0x8f, 0x19,
                                                0xda, 0x13, 0x66, 0xaf, 0x49, 0xd0, 0x90, 0x06,  0x8c, 0x6a, 0xfb, 0x91, 0x37, 0x8d, 0x0d, 0x78,
                                                0xbf, 0x49, 0x11, 0xf4, 0x23, 0xe5, 0xce, 0x3b,  0x55, 0xbc, 0xa2, 0x57, 0xe8, 0x22, 0x74, 0xce,
                                           };

            this.s_table[2] = new byte[64] {
                                                0x2c, 0xea, 0xc1, 0xbf, 0x4a, 0x24, 0x1f, 0xc2,  0x79, 0x47, 0xa2, 0x7c, 0xb6, 0xd9, 0x68, 0x15,
                                                0x80, 0x56, 0x5d, 0x01, 0x33, 0xfd, 0xf4, 0xae,  0xde, 0x30, 0x07, 0x9b, 0xe5, 0x83, 0x9b, 0x68,
                                                0x49, 0xb4, 0x2e, 0x83, 0x1f, 0xc2, 0xb5, 0x7c,  0xa2, 0x19, 0xd8, 0xe5, 0x7c, 0x2f, 0x83, 0xda,
                                                0xf7, 0x6b, 0x90, 0xfe, 0xc4, 0x01, 0x5a, 0x97,  0x61, 0xa6, 0x3d, 0x40, 0x0b, 0x58, 0xe6, 0x3d,
                                           };

            this.s_table[3] = new byte[64] {
                                                0x4d, 0xd1, 0xb2, 0x0f, 0x28, 0xbd, 0xe4, 0x78,  0xf6, 0x4a, 0x0f, 0x93, 0x8b, 0x17, 0xd1, 0xa4,
                                                0x3a, 0xec, 0xc9, 0x35, 0x93, 0x56, 0x7e, 0xcb,  0x55, 0x20, 0xa0, 0xfe, 0x6c, 0x89, 0x17, 0x62,
                                                0x17, 0x62, 0x4b, 0xb1, 0xb4, 0xde, 0xd1, 0x87,  0xc9, 0x14, 0x3c, 0x4a, 0x7e, 0xa8, 0xe2, 0x7d,
                                                0xa0, 0x9f, 0xf6, 0x5c, 0x6a, 0x09, 0x8d, 0xf0,  0x0f, 0xe3, 0x53, 0x25, 0x95, 0x36, 0x28, 0xcb,
                                           };
        }


        #region DES
        /// <summary>
        /// /// Initial permutation (IP).
        /// </summary>
        /// <param name="block"></param>
        /// <returns></returns>
        private byte[] IP(byte[] block)
        {
            byte[] tmp = new byte[block.Length];

            for (int i = 0; i < this.ip_table.Length; i++)
            {
                byte j = (byte)(this.ip_table[i] - 1);
                if ((block[(j >> 3) & 7] & this.mask[j & 7]) != 0)
                    tmp[(i >> 3) & 7] |= this.mask[i & 7];
            }

            return tmp;
        }

        /// <summary>
        /// Final permutation (IP^-1).
        /// </summary>
        /// <param name="src"></param>
        /// <returns></returns>
        private byte[] FP(byte[] src)
        {
            byte[] tmp = new byte[src.Length];

            for (int i = 0; i < this.fp_table.Length; i++)
            {
                int j = this.fp_table[i] - 1;
                if ((src[(j >> 3) & 7] & this.mask[j & 7]) != 0)
                    tmp[(i >> 3) & 7] |= this.mask[i & 7];
            }

            return tmp;
        }

        /// <summary>
        /// Expansion (E).
        /// Expands upper four 8-bits (32b) into eight 6-bits (48b).
        /// </summary>
        /// <param name="src"></param>
        /// <returns></returns>
        private byte[] E(byte[] src)
        {
            byte[] tmp = new byte[src.Length];

            tmp[0] = (byte)(((src[7] << 5) | (src[4] >> 3)) & 0x3f);	// ..0 vutsr
            tmp[1] = (byte)(((src[4] << 1) | (src[5] >> 7)) & 0x3f);	// ..srqpo n
            tmp[2] = (byte)(((src[4] << 5) | (src[5] >> 3)) & 0x3f);	// ..o nmlkj
            tmp[3] = (byte)(((src[5] << 1) | (src[6] >> 7)) & 0x3f);	// ..kjihg f
            tmp[4] = (byte)(((src[5] << 5) | (src[6] >> 3)) & 0x3f);	// ..g fedcb
            tmp[5] = (byte)(((src[6] << 1) | (src[7] >> 7)) & 0x3f);	// ..cba98 7
            tmp[6] = (byte)(((src[6] << 5) | (src[7] >> 3)) & 0x3f);	// ..8 76543
            tmp[7] = (byte)(((src[7] << 1) | (src[4] >> 7)) & 0x3f);	// ..43210 v

            return tmp;
        }

        /// <summary>
        /// Transposition (P-BOX).
        /// </summary>
        /// <param name="src"></param>
        /// <returns></returns>
        private byte[] TP(byte[] src)
        {
            byte[] tmp = new byte[src.Length];

            for (int i = 0; i < this.tp_table.Length; i++)
            {
                int j = this.tp_table[i] - 1;
                if ((src[(j >> 3) + 0] & mask[j & 7]) != 0)
                    tmp[(i >> 3) + 4] |= mask[i & 7];
            }

            return tmp;
        }

        /// <summary>
        /// Substitution boxes (S-boxes).
        /// NOTE: This implementation was optimized to process two nibbles in one step (twice as fast).
        /// </summary>
        /// <param name="src"></param>
        /// <returns></returns>
        private byte[] SBOX(byte[] src)
        {
            byte[] tmp = new byte[src.Length];

            int len = this.s_table.GetLength(0);
            for (int i = 0; i < len; i++)
                tmp[i] = (byte)((this.s_table[i][src[i * 2 + 0]] & 0xf0) | (this.s_table[i][src[i * 2 + 1]] & 0x0f));

            return tmp;
        }

        /// <summary>
        /// DES round function.
        /// XORs src[0..3] with TP(SBOX(E(src[4..7]))).
        /// </summary>
        /// <param name="src"></param>
        /// <returns></returns>
        private byte[] RoundFunction(byte[] src)
        {
            byte[] tmp = new byte[src.Length];
            Buffer.BlockCopy(src, 0, tmp, 0, src.Length);
            tmp = this.E(tmp);
            tmp = this.SBOX(tmp);
            tmp = this.TP(tmp);

            src[0] ^= tmp[4];
            src[1] ^= tmp[5];
            src[2] ^= tmp[6];
            src[3] ^= tmp[7];

            return src;
        }

        private byte[] DesDecryptBlock(byte[] block)
        {
            block = this.IP(block);
            block = this.RoundFunction(block);
            return this.FP(block);
        }

        private byte[] GrfShuffleDec(byte[] src)
        {
            byte[] output = new byte[8];

            output[0] = src[3];
            output[1] = src[4];
            output[2] = src[6];
            output[3] = src[0];
            output[4] = src[1];
            output[5] = src[2];
            output[6] = src[5];
            output[7] = GrfSubstitution(src[7]);

            return output;
        }

        /// <summary>
        /// Substitutes some specific values for others, leaves rest intact. Obfuscation.
        /// NOTE: Operation is symmetric (calling it twice gives back the original input).
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        private byte GrfSubstitution(byte input)
        {
            byte output;

            switch (input)
            {
                case 0x00: output = 0x2B; break;
                case 0x2B: output = 0x00; break;
                case 0x6C: output = 0x80; break;
                case 0x01: output = 0x68; break;
                case 0x68: output = 0x01; break;
                case 0x48: output = 0x77; break;
                case 0x60: output = 0xFF; break;
                case 0x77: output = 0x48; break;
                case 0xB9: output = 0xC0; break;
                case 0xC0: output = 0xB9; break;
                case 0xFE: output = 0xEB; break;
                case 0xEB: output = 0xFE; break;
                case 0x80: output = 0x6C; break;
                case 0xFF: output = 0x60; break;
                default: output = input; break;
            }

            return output;
        }

        #endregion


        public byte[] GrfDecodeHeader(byte[] data, int len)
        {
            int nblocks = len / BLOCK_SIZE;
            int i, block_offset;
            byte[] block = new byte[BLOCK_SIZE];

            // first 20 blocks are all des-encrypted
            for (i = block_offset = 0; i < 20 && i < nblocks; ++i, block_offset += 8)
            {
                Buffer.BlockCopy(data, block_offset, block, 0, block.Length);
                block = this.DesDecryptBlock(block);
                Buffer.BlockCopy(block, 0, data, block_offset, block.Length);
            }

            // the rest is plaintext, done.

            return data;
        }

        /// <summary>
        /// Decodes grf data.
        /// </summary>
        /// <param name="data"></param>
        /// <param name="cycle"></param>
        /// <returns></returns>
        public byte[] GrfDecodeFull(byte[] data, int cycle)
        {
            int nblocks = data.Length / BLOCK_SIZE;
            int dcycle, scycle;
            int i, j, block_offset = 0;
            byte[] block = new byte[BLOCK_SIZE];

            // first 20 blocks are all des-encrypted
            for (i = block_offset = 0; i < 20 && i < nblocks; ++i, block_offset += 8)
            {
                Buffer.BlockCopy(data, block_offset, block, 0, block.Length);
                block = this.DesDecryptBlock(block);
                Buffer.BlockCopy(block, 0, data, block_offset, block.Length);
            }

            // after that only one of every 'dcycle' blocks is des-encrypted
            dcycle = cycle;

            // and one of every 'scycle' plaintext blocks is shuffled (starting from the 0th but skipping the 0th)
            scycle = 7;

            // so decrypt/de-shuffle periodically
            j = -1; // 0, adjusted to fit the ++j step

            for (i = 20, block_offset = 160; i < nblocks; ++i, block_offset += 8)
            {
                if (i % dcycle == 0) // decrypt block
                {
                    Buffer.BlockCopy(data, block_offset, block, 0, block.Length);
                    block = this.DesDecryptBlock(block);
                    Buffer.BlockCopy(block, 0, data, block_offset, block.Length);
                    continue;
                }

                ++j;
                if (j % scycle == 0 && j != 0) // de-shuffle block
                {
                    Buffer.BlockCopy(data, block_offset, block, 0, block.Length);
                    block = this.GrfShuffleDec(block);
                    Buffer.BlockCopy(block, 0, data, block_offset, block.Length);
                    continue;
                }

                // plaintext, do nothing.
            }
            return data;
        }
    }
}
